Skip to content

QVAC-21320 feat: Mali GPU projector optimizations — disable FA, warptile, layernorm fusion#174

Merged
gianni-cor merged 4 commits into
temp-9341from
feat/QVAC-21320-mali-vulkan-projector-opt
Jul 7, 2026
Merged

QVAC-21320 feat: Mali GPU projector optimizations — disable FA, warptile, layernorm fusion#174
gianni-cor merged 4 commits into
temp-9341from
feat/QVAC-21320-mali-vulkan-projector-opt

Conversation

@iancris

@iancris iancris commented Jun 28, 2026

Copy link
Copy Markdown

🎯 What problem does this PR solve?

Mali-G715 GPU vision projector (mmproj/image encoder) is ~46% slower than CPU due to:

  • Flash attention scalar path (FA_SCALAR) being ~2.6x less efficient than the matmul path (38 vs ~100 GFLOPS/s) — FA is ~30–56% of encode time (grows with resolution)
  • Suboptimal warptile for q8_0 MMQ matmuls on Mali/Valhall (~90 GFLOPS/s)
  • Redundant dispatch overhead from unfused layernorm ops

📝 How does it solve it?

Three quality-neutral ggml-vulkan optimizations (Qwen3.5-0.8B, Pixel 9 / Mali-G715):

  1. Disable FA on GPU projectors with inefficient flash attention (tools/mtmd/clip.cpp + ggml-vulkan.cpp): the projector queries the backend at runtime (ggml_backend_reg_get_proc_address("ggml_backend_supports_efficient_fa")). Vendor-aware: only ggml-vulkan implements the query, returning false for VK_VENDOR_ID_ARM (Mali advertises VK_KHR_cooperative_matrix but its FA still runs the slow path) → Mali uses explicit softmax; real-coopmat Vulkan GPUs keep FA. Backends that don't implement the query (Metal/CUDA/CPU) keep FA (they have efficient FA) — the gate defaults to keeping FA and disables only on an affirmative not-efficient answer.
  2. Mali/Valhall warptile tuning (VK_VENDOR_ID_ARM): large q8_0 MMQ tile to 32-warp/16-wide (~90 → ~124 GFLOPS/s); self-disables if shared memory is insufficient.
  3. NORM+MUL+ADD (layernorm) Vulkan fusion: one dispatch replaces three (−26/encode); NOT Mali-specific.

Results (4-run CPU-matched benchmark, profiler-off, Pixel 9 / Mali-G715, temp-9341; 15-image lmms-eval fixture, n=15/cell)

Within-run GPU/CPU mmproj-encode ratio (CPU & GPU on the same device — the device-normalized robust metric):

Build per-run ratios mean
Baseline (stock temp-9341) 1.462× / 1.453× ~1.46×
Optimized (this PR) 1.114× / 1.094× ~1.10×

CPU-matched same-unit-equivalent absolutes (best CPU-fingerprint pair, 5.0% apart):

Metric Baseline-CPU Baseline-GPU Optimized-CPU Optimized-GPU GPU: opt vs base Opt-GPU vs Base-CPU
mmproj-encode (ms) 5960.8 8661.2 5669.4 6202.7 +28.4% faster −4.1% slower
TTFT (ms) 10128 12741 9125 9563 +24.9% faster +5.6% faster
wall (ms) 10269 12863 9283 9696 +24.6% faster +5.6% faster

GPU clip-encode by resolution (per-op profile, FA-on baseline → FA-off optimized):

n_pos Baseline (FA on) Optimized (FA off) Δ
1040 3108 ms 2347 ms −24%
2688 11583 ms 8077 ms −30%
3072 13862 ms 8997 ms −35%
3720 19445 ms 12561 ms −35%

Quality 37.8% == 37.8% (Δ=0) in all runs. Per-op profiling confirms FLASH_ATTN_EXT (~40 GFLOPS/s) is
replaced by SOFT_MAX + matmuls. Net: the Mali GPU projector goes from ~46% slower than its own-device CPU to
~10% (near-parity), and beats the CPU projector that ships today on TTFT (+5.6%) while freeing the CPU
for the LLM.

🧪 How was it tested?

Per-op Vulkan profiling + a 4-run CPU-matched benchmark on Pixel 9 (Mali-G715) over the frozen 15-image
lmms-eval fixture (base preset, n=15/cell, 0 fail), on current temp-9341:

  • Profile (profiler-on): FLASH_ATTN_EXTSOFT_MAX; GPU clip-encode −24…−35% by resolution.
  • 4-run CPU-matched (profiler-off): baseline temp-9341 vs this branch — within-run GPU/CPU ratio
    ~1.46× → ~1.10×; CPU-matched cross GPU mmproj-encode +28.4%, TTFT +24.9%; quality Δ=0. Re-confirmed after
    the Metal fix (5b0771040): within-run GPU/CPU ~1.11×, quality-neutral (Mali/Vulkan path unchanged).

Metal fix (5b0771040): an earlier form of the FA-gate defaulted to disabling FA when a backend didn't
answer the query, which wrongly disabled FA on Metal (the query is Vulkan-only) → explicit attention overflowed
the clip compute buffer (GGML_ASSERT in ggml-backend, e.g. image_tile_mode=disabled + large
image_max_tokens) crashing darwin/iOS. Fixed by defaulting to keep FA on backends without the query;
darwin-arm64 integration re-validated green.

Phase-A cross-consumer overlay CI — re-validated on the current head a96944bdd (branch rebased onto
current temp-9341 6613cea17 — 36 upstream commits, identical replay verified via git range-diff — plus
the tiled-NORM review fix) across all 6 fabric consumers via workflow_dispatch (vcpkg overlay-port, no
registry/baseline change). All consumers' builds/prebuilds (all platforms) + C++ tests + desktop + mobile
integration pass:

Known non-blockers (not regressions from this PR): merge-guard/validate-pr fails on every run (expected — a
workflow_dispatch branch has no PR); test-darwin-x64 is pre-existing Intel-Mac-runner instability that
reproduces on the baseline fabric (no opts); the intermittent llm-llamacpp qwen3-5-multimodal-cache-stress
(also baseline-reproducible) passed this round. Device-Farm mobile-monitor and runner-provisioning flakes
cleared on re-run (embed linux-arm64, ocr Android, nmtcpp iOS).

⚠️ Breaking changes

None. Warptile self-guards on shared memory. The FA gate is additive and vendor-scoped (Metal/CUDA and
coopmat desktop Vulkan keep FA; only inefficient-FA GPUs like Mali disable it). Layernorm fusion is
accuracy-neutral.


@iancris iancris requested a review from a team as a code owner June 28, 2026 10:57
@iancris iancris force-pushed the feat/QVAC-21320-mali-vulkan-projector-opt branch from ba40f87 to e647cd8 Compare June 28, 2026 18:54
@iancris iancris marked this pull request as draft June 29, 2026 15:54
@iancris iancris marked this pull request as ready for review July 1, 2026 17:08
iancris and others added 3 commits July 6, 2026 13:10
…ile, layernorm fusion

Three quality-neutral ggml-vulkan optimizations for the Mali-G715 vision projector
(mmproj/image encoder), benchmarked on Pixel 9 Pro (Qwen3.5-0.8B):

1. Disable flash attention on GPU projectors without efficient (coopmat) FA
   (tools/mtmd/clip.cpp). Uses runtime proc_address resolution to query the
   backend — no compile-time backend dependency. Mali FA_SCALAR ~2.6x less
   efficient than the matmul path; coopmat-capable GPUs keep FA enabled.

2. Mali/Valhall warptile tuning (ggml-vulkan.cpp, VK_VENDOR_ID_ARM)
   — large q8_0 MMQ tile to 32-warp/16-wide layout; ~90->~124 GFLOPS/s.
   Self-disables if shared memory is insufficient. Vendor-wide: also speeds
   the LLM prefill on GPU.

3. NORM+MUL+ADD (layernorm) Vulkan fusion (norm.comp, generic_binary_head.glsl,
   vulkan-shaders-gen.cpp, ggml-vulkan.cpp) — one dispatch replaces three;
   mirrors rms_norm+mul. -26 dispatches/encode. NOT Mali-specific.

Result (4-run CPU-matched, profiler-off): within-run GPU/CPU mmproj-encode
ratio ~1.46x (baseline) -> ~1.12x (optimized); near-parity 1.09x at high
resolution. Quality 37.8%==37.8% (delta=0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fficient FA)

The coopmat-quality FA-gate added in this PR was inert on Mali: Mali-G715
advertises VK_KHR_cooperative_matrix, so coopmat1_fa_support is true and
ggml_backend_vk_supports_efficient_fa() returned true — yet Mali's flash
attention still runs the slow path (~40 GFLOPS/s vs the ~100 GFLOPS/s matmul
path). Per-op profiling on temp-9341/Pixel 9 showed the "optimized" build's
clip encode was byte-for-byte identical to baseline (FLASH_ATTN_EXT present,
zero SOFT_MAX) — the FA-disable never fired.

Fix:
- ggml-vulkan.cpp: ggml_backend_vk_supports_efficient_fa() returns false for
  VK_VENDOR_ID_ARM (Mali coopmat is not fast FA). Real-coopmat desktop GPUs
  (NVIDIA/AMD/Intel) still report efficient FA and keep it.
- clip.cpp: apply the gate on flash_attn_type != DISABLED (the addon enables FA
  by default, not AUTO-only) and default to disabling FA when the backend can't
  confirm efficient FA (the safe original behaviour).

Verified on temp-9341/Pixel 9 (Mali-G715): FLASH_ATTN_EXT -> SOFT_MAX, GPU
clip-encode -30..35% at high resolution; 4-run CPU-matched within-run GPU/CPU
mmproj-encode ratio 1.46x (baseline) -> 1.10x (optimized), quality neutral
(37.8% == 37.8%) — recovering the original 1.46x->1.12x result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ry (Metal/CUDA)

The FA-gate defaulted efficient_fa=false, so on non-Vulkan GPU backends (Metal, CUDA) — where ggml_backend_supports_efficient_fa is not implemented — flash attention was wrongly disabled. The resulting explicit-attention clip path overflows the pre-sized compute buffer at high n_pos (image_tile_mode=disabled + image_max_tokens=4096), hitting GGML_ASSERT in ggml-backend.cpp:2043 (SIGABRT) on darwin/iOS Metal integration tests.

Fix: default efficient_fa=true and disable only when a backend affirmatively reports non-efficient FA. Only ggml-vulkan implements the query (returns false for VK_VENDOR_ID_ARM / non-coopmat), so the Mali FA-disable win is unchanged; Metal/CUDA/CPU keep their efficient FA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iancris iancris force-pushed the feat/QVAC-21320-mali-vulkan-projector-opt branch from 9670eae to 5b07710 Compare July 6, 2026 13:12
@iancris iancris requested review from a team as code owners July 6, 2026 13:12
Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
…pCount

Review fix (PR #174): the fused-norm change dispatched GGML_OP_NORM as a direct {ne01, ne02, ne03} grid; on large row counts ne01 can exceed maxComputeWorkGroupCount[0] (spec minimum 65535) and trip the GGML_ASSERT in ggml_vk_dispatch_pipeline, where the previous flattened/tiled dispatch handled arbitrary ggml_nrows.

Restore the flattened {512, 512, N} row tiling on the host (same group as SOFT_MAX/SUM_ROWS) and reconstruct {row, channel, sample} in norm.comp from the flat workgroup id (formula shared with soft_max.comp), with a workgroup-uniform bounds return for the tiling round-up. dst offset is unchanged: flat_row == (samp*nchannels + channel)*nrows + row by construction. No behavioural change for in-range shapes; the fusion's dispatch-count reduction is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gianni-cor gianni-cor merged commit 42350a5 into temp-9341 Jul 7, 2026
35 of 53 checks passed
makaveli10 pushed a commit to makaveli10/qvac-ext-lib-llama.cpp that referenced this pull request Jul 13, 2026
…pCount

Review fix (PR tetherto#174): the fused-norm change dispatched GGML_OP_NORM as a direct {ne01, ne02, ne03} grid; on large row counts ne01 can exceed maxComputeWorkGroupCount[0] (spec minimum 65535) and trip the GGML_ASSERT in ggml_vk_dispatch_pipeline, where the previous flattened/tiled dispatch handled arbitrary ggml_nrows.

Restore the flattened {512, 512, N} row tiling on the host (same group as SOFT_MAX/SUM_ROWS) and reconstruct {row, channel, sample} in norm.comp from the flat workgroup id (formula shared with soft_max.comp), with a workgroup-uniform bounds return for the tiling round-up. dst offset is unchanged: flat_row == (samp*nchannels + channel)*nrows + row by construction. No behavioural change for in-range shapes; the fusion's dispatch-count reduction is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gianni-cor gianni-cor deleted the feat/QVAC-21320-mali-vulkan-projector-opt branch July 15, 2026 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants